using System;
using System.Collections.Generic;
using System.Text;
using System.Windows.Forms;
using System.Drawing;
using System.Threading;
using System.Runtime.InteropServices;
using DirectShowLib;
using Microsoft.DirectX;
using Microsoft.DirectX.Direct3D;

namespace MultiVMR9cs
{
    public delegate void DeviceLostCallback();

    public class MultiVMR9Renderer : IDisposable
    {
        private int _adapter;
        private Control _control;
        private Size _backbufferSize;
        private PresentParameters _presentParam;
        private Device _device = null;
        private Thread _renderThread;
        private int _fps;
        private Format _format;
        private RenderThreadStatus _renderThreadStatus = RenderThreadStatus.eNotStarted;
        private List<VideoSource> _sources = new List<VideoSource>();
        private SynchronizationContext context;
        private bool disposed = false;
        private float _masterVolume = 1f;
        private const int DxMagicNumber = -759872593;
        private object _lockObject = new object();

        private FullScreenMode _fullscreen = FullScreenMode.None;
        private Form _fullscreenForm;

        public MultiVMR9Renderer(Control c, VMR9DisplayMode mode)
        {
            // Get the SynchronizationContext for this thread so the Renderthread can call the
            // DeviceLost method on the same thread the device was created on. (say that 10 times fast)
            context = SynchronizationContext.Current;

            // Turn off event handling for the MDX runtime, this will
            // increase performance and remove potential working set issues.
            Device.IsUsingEventHandlers = false;

            _control = c;
            _adapter = mode.Adapter;
            _fps = mode.RefreshRate;
            _backbufferSize = new Size(mode.Width, mode.Height);
            _format = mode.Format;
            CreateDevice();
        }

        public Size BackBufferSize
        {
            get
            {
                return _backbufferSize;
            }
        }

        /// <summary>
        /// Gets or Sets the FullScreen Mode of the Renderer
        /// </summary>
        public FullScreenMode FullScreenMode
        {
            get
            {
                return _fullscreen;
            }
            set
            {
                if (value != _fullscreen)
                {
                    _fullscreen = value;
                    switch (value)
                    {
                        case FullScreenMode.None:
                            {
                                ResetDevice(false);
                                if (_fullscreenForm != null)
                                {
                                    _fullscreenForm.Dispose();
                                    _fullscreenForm = null;
                                }
                            }
                            break;
                        case FullScreenMode.WindowedFullScreen:
                            break;
                        case FullScreenMode.FullScreen:
                            {
                                if (_fullscreenForm == null)
                                {
                                    _fullscreenForm = new Form();
                                    _fullscreenForm.Show();
                                }
                                ResetDevice(false);
                            }
                            break;
                        default:
                            break;
                    }                    
                }
            }
        }

        /// <summary>
        /// Get the DirectX device for this renderer
        /// </summary>
        public Device Device
        {
            get
            {
                return _device;
            }
        }

        /// <summary>
        /// Gets or Sets the PresentParameters for the Device.  Setting will cause a Device Reset
        /// </summary>
        public PresentParameters PresentParameters
        {
            get
            {
                return _presentParam;
            }
            set
            {
                if (value != null)
                {
                    _presentParam = value;
                    ResetDevice(false);
                }
            }
        }

        /// <summary>
        /// Gets or sets the Master Volume for all Video Sources in the renderer
        /// </summary>
        public float MasterVolume
        {
            get
            {
                return _masterVolume;
            }
            set
            {
                _masterVolume = Math.Min(1f, Math.Max(0f, value));

                // scale the volume of all attached sources
                foreach (VideoSource source in _sources)
                {
                    source.setVolume(_masterVolume * source.Volume);
                }
            }
        }

        /// <summary>
        /// Add a Video Source to the Renderer
        /// </summary>
        /// <param name="source"></param>
        public void AddSource(VideoSource source)
        {
            lock (_sources)
            {
                _sources.Add(source);
            }
        }

        /// <summary>
        /// Remove a Video Source in the Renderer
        /// </summary>
        /// <param name="source"></param>
        public void RemoveSource(VideoSource source)
        {
            lock (_sources)
            {
                _sources.Remove(source);
            }
        }

        /// <summary>
        /// Get the list of video sources attached to the renderer
        /// </summary>
        public List<VideoSource> VideoSources
        {
            get
            {
                return _sources;
            }
        }

        /// <summary>
        /// Get a list of sources that are a clone of a given videoSource in the order they can be removed.
        /// You wouldn't want to remove a video source if it still has clones in the renderer.
        /// </summary>
        /// <param name="videoSource"></param>
        /// <returns></returns>
        public List<CloneSource> GetClones(VideoSource videoSource)
        {
            return GetClones(videoSource, new List<CloneSource>());
        }

        private List<CloneSource> GetClones(VideoSource videoSource,List<CloneSource> clones)
        {
            //check to see if there are any clones of this source
            foreach (VideoSource vs in _sources)
            {
                if (vs is CloneSource && (vs as CloneSource)._clonedSource == videoSource)
                {
                    clones = GetClones(vs, clones);
                }
            }

            //add this one to the end of the list
            if (videoSource is CloneSource)
            {
                clones.Add((CloneSource)videoSource);
            }

            return clones;
        }

        /// <summary>
        /// Get the order a video source is drawn
        /// </summary>
        /// <param name="source"></param>
        /// <returns></returns>
        internal int GetZOrder(VideoSource source)
        {
            return _sources.IndexOf(source);
        }

        /// <summary>
        /// Set the order a video source is drawn
        /// </summary>
        /// <param name="source"></param>
        /// <param name="zorder"></param>
        internal void SetZOrder(VideoSource source, int zorder)
        {
            lock (_sources)
            {
                _sources.Remove(source);
                _sources.Insert(zorder, source);
            }
        }

        #region IDisposable & Destructor

        ~MultiVMR9Renderer()
        {
            Dispose(true);
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        private void Dispose(bool disposing)
        {
            if (!disposed)
            {   
                disposed = true;
            }
        }

        #endregion

        #region Private Methods

        private void CreateDevice()
        {
            _presentParam = new PresentParameters();
            _presentParam.PresentationInterval = PresentInterval.Immediate;
            _presentParam.Windowed = true;
            _presentParam.DeviceWindowHandle = _control.Handle;
            _presentParam.SwapEffect = SwapEffect.Discard;
            _presentParam.PresentFlag = PresentFlag.LockableBackBuffer | PresentFlag.Video | PresentFlag.DiscardDepthStencil | PresentFlag.DeviceClip;
            _presentParam.BackBufferCount = 1;
            _presentParam.EnableAutoDepthStencil = true;
            _presentParam.AutoDepthStencilFormat = DepthFormat.D16;
            _presentParam.BackBufferWidth = _backbufferSize.Width;
            _presentParam.BackBufferHeight = _backbufferSize.Height;
            _presentParam.BackBufferFormat = _format;

            _device = new Device(
                _adapter,
                DeviceType.Hardware,
                _control.Handle,
                CreateFlags.HardwareVertexProcessing | CreateFlags.MultiThreaded,
                _presentParam
                );

            InitDevice();

            // start up the render thread
            _renderThread = new Thread(new ThreadStart(this.RenderLoop));
            _renderThread.IsBackground = true;
            _renderThread.Start();
        }

        /// <summary>
        /// Initialize the device's view and render states
        /// </summary>
        private void InitDevice()
        {
            // set up the View
            float aspect = (float)_backbufferSize.Width / (float)_backbufferSize.Height;
            Matrix matProj = Matrix.PerspectiveFovLH((float)Math.PI / 4, aspect, 1.0f, 100.0f);
            _device.SetTransform(TransformType.Projection, matProj);
            Vector3 from = new Vector3(0.0f, 0.0f, -2.415f);
            Vector3 at = new Vector3(0.0f, 0.0f, 0.0f);
            Vector3 up = new Vector3(0.0f, 1.0f, 0.0f);
            Matrix matView = Matrix.LookAtLH(from, at, up);
            _device.SetTransform(TransformType.View, matView);

            // set the Fixed Pipeline RenderStates
            _device.RenderState.CullMode = Cull.None;
            _device.RenderState.Lighting = false;
            _device.RenderState.AlphaBlendEnable = true;
            _device.RenderState.SourceBlend = Blend.SourceAlpha;
            _device.RenderState.DestinationBlend = Blend.InvSourceAlpha;
            _device.RenderState.AlphaTestEnable = true;
            _device.RenderState.ReferenceAlpha = 0x10;
            _device.RenderState.AlphaFunction = Compare.Greater;
            _device.RenderState.ZBufferWriteEnable = true;
            _device.RenderState.ZBufferEnable = true;
            _device.RenderState.ZBufferFunction = Compare.LessEqual;
            _device.RenderState.StencilEnable = false;
        }

        public void ResetDevice(VMR9DisplayMode mode)
        {
            // see if we need to recreate the device for a new adapter
            bool adapterChange = _adapter != mode.Adapter;

            float currentAR = (float)_backbufferSize.Width / (float)_backbufferSize.Height;

            _format = mode.Format;
            _fps = mode.RefreshRate;

            _backbufferSize = new Size(mode.Width, mode.Height);
            _adapter = mode.Adapter;
            ResetDevice(adapterChange);

            //if the aspect ratio changed, notify all the sources
            if(currentAR != ((float)mode.Width / (float)mode.Height))
            {
                lock(_sources)
                {
                    foreach(VideoSource source in _sources)
                    {
                        source.OnAspectRatioChanged(new Size(mode.Width,mode.Height));
                    }
                }
            }
        }

        public void ResetDevice(bool RecreateDevice)
        {
            lock (_lockObject)
            {
                bool _renderThreadRunning = false;

                Control c = _fullscreen == FullScreenMode.None ? _control : _fullscreenForm;
                _presentParam.DeviceWindowHandle = c.Handle;

                // stop the render thread if one is running
                if (_renderThread != null)
                {
                    _renderThreadRunning = true;
                    if (_renderThreadStatus != RenderThreadStatus.eWaitingToStop)
                    {
                        // this was called by a manual reset, so kill the render thread
                        _renderThreadStatus = RenderThreadStatus.eWaitingToStop;
                        while (_renderThreadStatus != RenderThreadStatus.eFinished)
                        {
                            // wait for the render thread to die
                            Thread.Sleep(20);
                        }
                    }
                    _renderThread = null;
                }

                // we'll keep a list of the current state of all the graphs so we can restore them after the reset
                List<FilterState> states = new List<FilterState>();
                if (_sources.Count > 0)
                {
                    // stop all the graphs and store the current state
                    foreach (VideoSource source in _sources)
                    {
                        FilterState fs = FilterState.Stopped;
                        if (source._mediaControl != null)
                        {
                            source._mediaControl.GetState(0, out fs);
                        }

                        // call the mediacontrol stop directly as not to raise the stop event
                        if (source._mediaControl != null)
                        {
                            source._mediaControl.Stop();
                        }
                        states.Add(fs);
                    }

                    // purge our allocators of the lost device and any resources the video source created
                    foreach (VideoSource source in _sources)
                    {
                        source.OnDeviceLost();
                    }
                }

                // modify the presentation parameters to any new states
                if (_fullscreen == FullScreenMode.FullScreen)
                {
                    _presentParam.Windowed = false;
                    // we want a PresentInterval of one to prevent tearing in exculsive fullscreen mode
                    _presentParam.PresentationInterval = PresentInterval.One;
                    _presentParam.FullScreenRefreshRateInHz = _fps;
                }
                else
                {
                    _presentParam.Windowed = true;
                    _presentParam.PresentationInterval = PresentInterval.Immediate;
                    _presentParam.FullScreenRefreshRateInHz = 0;
                }
                _presentParam.BackBufferWidth = _backbufferSize.Width;
                _presentParam.BackBufferHeight = _backbufferSize.Height;
                _presentParam.BackBufferFormat = _format;                

                // perform garbage collection to ensure all resources are gone
                GC.Collect();
                GC.WaitForPendingFinalizers();

                if (!RecreateDevice)
                {
                    // wait for the device to become available for reset
                    int result;
                    _device.CheckCooperativeLevel(out result);
                    if (result != 0)
                    {
                        while (result != (int)ResultCode.DeviceNotReset)
                        {
                            Thread.Sleep(20);
                            _device.CheckCooperativeLevel(out result);
                        }
                    }

                    // reset the device and re-init
                    try
                    {
                        _device.Reset(_presentParam);
                    }
                    catch (Exception ex)
                    {
                        MessageBox.Show(ex.Message, "Error Reseting 3D Device");
                    }

                    // wait for the device to become available again
                    while (!_device.CheckCooperativeLevel())
                    {
                        Thread.Sleep(20);
                    }
                }
                else
                {
                    // we need to recreate the entire device
                    _device.Dispose();
                    _device = new Device(
                        _adapter,
                        DeviceType.Hardware,
                        c.Handle,
                        CreateFlags.HardwareVertexProcessing | CreateFlags.MultiThreaded,
                        _presentParam
                        );

                    // tell each source that we have a new Device
                    foreach (VideoSource source in _sources)
                    {
                        source.OnDeviceRecreated(_device);
                    }
                }

                // re-init view and render states
                InitDevice();

                if (_sources.Count > 0)
                {
                    // recreate DirectX resources
                    foreach (VideoSource source in _sources)
                    {
                        source.OnDeviceReset();
                    }
                }

                // restore the running states to all the graphs
                for (int i = 0; i < _sources.Count; i++)
                {
                    if (states[i] == FilterState.Paused)
                    {
                        _sources[i]._mediaControl.Pause();
                    }
                    else if (states[i] == FilterState.Running)
                    {
                        _sources[i]._mediaControl.Run();
                    }
                }

                // restart the render thread if one was started
                if (_renderThreadRunning)
                {
                    _renderThread = new Thread(new ThreadStart(this.RenderLoop));
                    _renderThread.IsBackground = true;
                    _renderThread.Start();
                }
            }
        }

        /// <summary>
        /// The render loop thread
        /// </summary>
        public void RenderLoop()
        {
            int frame = 0;
            _renderThreadStatus = RenderThreadStatus.eRunning;

            while (_renderThreadStatus != RenderThreadStatus.eWaitingToStop)
            {
                frame++;
                _device.BeginScene();
                
                // clear the scene so we don't have any artifacts left
                _device.Clear(ClearFlags.ZBuffer | ClearFlags.Target, 0x00000000, 1f, 0);

                lock (_sources)
                {
                    // compose the sources
                    foreach (VideoSource source in _sources)
                    {
                        source.OnCompose();
                    }

                    // draw the sources
                    foreach (VideoSource source in _sources)
                    {
                        source.OnDraw();
                    }
                }

                _device.EndScene();

                try
                {
                    _device.Present();
                }
                catch (DeviceLostException)
                {
                    _renderThreadStatus = RenderThreadStatus.eWaitingToStop;
                    // because the device was created on a different thread, we have to invoke
                    // the ResetDevice method on the thread it was created on.
                    context.Send(new SendOrPostCallback(delegate(object state)
                    {
                        this.ResetDevice(false);
                    }), null);
                    break;
                }
                catch(Exception ex)
                {
#if DEBUG
                    Console.WriteLine("Error in Present: " + ex.Message);
#endif
                }

                Thread.Sleep(10);
            }
            _renderThreadStatus = RenderThreadStatus.eFinished;
        }

        #endregion
    }

    public enum FullScreenMode
    {
        None,
        WindowedFullScreen,
        FullScreen
    }

    internal enum RenderThreadStatus
    {
        eNotStarted = 0x0,
        eRunning,
        eWaitingToStop,
        eFinished,
    }
}
